package ferp.android.activities;

import android.app.Activity;
import android.app.Dialog;
import android.content.*;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import ferp.android.BuildConfig;
import ferp.android.GUI;
import ferp.android.R;
import ferp.android.RestoreGame;
import ferp.android.activities.preferences.Preferences;
import ferp.android.ads.MediationAdManager;
import ferp.android.ads.interstitial.InterstitialAdManager;
import ferp.android.dialogs.*;
import ferp.android.engagement.ApplicationRater;
import ferp.android.managers.*;
import ferp.android.services.IResolverService2;
import ferp.android.services.ResolverService2;
import ferp.android.services.ServiceException;
import ferp.android.services.ServiceProxy;
import ferp.android.social.Connector;
import ferp.android.tasks.center.TaskBidAdd;
import ferp.android.tasks.center.TaskCenterConfigGet;
import ferp.android.tasks.center.TaskGameAdd;
import ferp.android.tasks.center.TaskGameReportSend;
import ferp.android.tasks.local.*;
import ferp.android.views.TableView;
import ferp.core.ai.tree2.Score;
import ferp.core.card.Card;
import ferp.core.game.*;
import ferp.core.game.Statistics;
import ferp.core.log.Log;
import ferp.core.mode.Dropping;
import ferp.core.player.Hand;
import ferp.core.player.Player;
import ferp.core.player.Profile;
import ferp.core.state.State;

import java.util.Arrays;

public class Ferp extends Activity implements Game.Listener,
                                              TableView.Listener,
                                              PoolDialog.Listener,
                                              ErrorDialog.Listener,
                                              RadioListViewDialog.Listener,
                                              OfferDialog.Listener,
                                              TaskGameReportSend.Listener,
                                              ReportGameDialog.Listener,
                                              MessageDialog.Listener,
                                              TaskDeckLoader.Listener,
                                              PollManager.Listener
{
  public void exit(ServiceException e)
  {
    boolean ner = e.isDeadObject() || e.status() == ResolverService2.OUT_OF_MEMORY;
    int dialog = ner ? Dialogs.NOT_ENOUGH_RESOURCES : Dialogs.ALERT_ERROR;

    exit(e, e.getMessage(), dialog, !ner);
  }

  @Override
  public void onNewGameStarted() {LoadingDialog.dismiss();}

  @Override
  public void onGameResumeRequested(Game g)
  {
    if (service != null)
      RestoreGame.execute(this, g, getCurrentProfile().settings, service, table, menuPreviousTrick);
  }

  @Override
  public void onGameResumeStarted(Game g) {LoadingDialog.show(this, R.string.resuming_game);}

  @Override
  public void onGameResumeFinished(Game g)
  {
    // call 'load' method to ensure interstitial ad loading as 'onAfterDeal' not called when game resumed
    InterstitialAdManager.load();

    LoadingDialog.dismiss();

    // always clear 'WAIT_FOR_RESUMING'
    g.options.clear(Input.Options.WAIT_FOR_RESUMING);

    if (!g.options.isHumanInputRequired())
      process.now();
  }

  @Override
  public void onReplayStarted(Game g)
  {
    LoadingDialog.dismiss();

    onAfterDeal(g);
  }

  @Override
  public void onFastMovingRequested(Game g)
  {
    Profile p = getCurrentProfile();

    if (service != null)
      new TaskFastMoving(this, g, p.settings, service).execute();
  }

  @Override
  public void onFastMovingStarted(Game g)
  {
    showDialog(Dialogs.FAST_MOVING);
  }

  @Override
  public void onFastMovingFinished(Game g)
  {
    GUI.tryToDismissDialog(this, Dialogs.FAST_MOVING);

    if (!g.options.isHumanInputRequired())
      process.now();
  }

  public void onAfterDeal(Game g)
  {
    // load interstitial ad to be ready to show it when game is over
    InterstitialAdManager.load();

    // set correct enable state to 'Previous trick' context menu option
    if (menuPreviousTrick != null)
      menuPreviousTrick.setEnabled(false);

    Profile profile = getCurrentProfile();

    // reset table view
    table.reset(g, profile.settings);

    // show poll
    if (PollManager.get() != null)
      showDialog(Dialogs.POLL);
  }

  public void onBiddingStart(Game g)
  {
    table.showFirstHand(g.player.starting);
  }

  public void onPlayerBid(Player p, Game.BidData bid)
  {
    table.showBid(p, bid, true);

    //if (p != pm.getCurrentProfile().game.human())
    process.delay = Settings.Delays.AFTER_BID;

    Log.debug(Log.TAG, p.name() + ": " + bid.toString());
  }

  @SuppressWarnings("unchecked")
  public void onFirstHumanBid(Game g, Player human)
  {
    // do not contribute in debug
    if (!BuildConfig.DEBUG)
    {
      Profile profile = getCurrentProfile();

      // do not contribute if "useCenter" not enabled or opponent levels are not "Expert"
      if (profile.useCenter && !profile.hacked && profile.centerId > 0 && profile.settings.levels[0].ordinal() == 0 && profile.settings.levels[1].ordinal() == 0)
      {
        Statistics statistics = profile.getStatistics();
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplication());
        float ratio = statistics.totalPool > 0 ? (float)statistics.totalAmount / (float)statistics.totalPool : 0;

        // to contribute bids the ratio must be greater than the threshold defined in the remote configuration
        if (ratio > TaskCenterConfigGet.getBidContributionThreshold(preferences))
          new TaskBidAdd(profile, g).execute();
        else
          Log.debug(Log.TAG, "player's score/pool ratio [" + statistics.totalAmount + "/" + statistics.totalPool + "] is lower than threshold");
      }
    }
  }

  public void onBiddingEnd()
  {
  }

  public void onContractingEnd()
  {
  }

  public void onTalonShow(Game g, Settings settings)
  {
    // show talon cards
    table.openTalon();

    // wait for touch
    g.options.set(Input.Options.WAIT_FOR_TOUCH);

    // set longer delay for next process()
    //process.delay = pm.getCurrentProfile().game.declarer == pm.getCurrentProfile().game.human() ? Process.Delay.AFTER_HUMAN_TALON_OPEN : Process.Delay.AFTER_AI_TALON_OPEN;
  }

  public void onTalonTaken(Game g, Settings settings)
  {
    table.hideTalon();
    table.showPlayerCards(g, settings, g.player.declarer);

    // AI plays misere - show misere desk
    if (g.bid.isMisere() && g.declarer() != g.human())
      table.showMisereDesk(g, settings, g.getDeclarerHand().current, getCurrentProfile().isLandscape());
  }

  public void onPlayerContract(Player p, Game.BidData bid)
  {
    table.showBid(p, bid, true);

    //if (p != pm.getCurrentProfile().game.human())
    process.delay = Settings.Delays.AFTER_CONTRACT;

    Log.debug(Log.TAG, p.name() + ": " + bid.toString());
  }

  public void onDroppingStart(Game g)
  {
    // hide player bids for tricking only - misere bids have to stay
    if (!g.bid.isMisere())
      table.hideBids(g);
  }

  public void onPlayerDrop(Game g, Settings settings, Player p)
  {
    table.showPlayerCards(g, settings, p.id());
    table.enablePlayerCards(p.id(), false);

    if (p != g.human())
      process.delay = Settings.Delays.AFTER_DROP;
  }

  public void onOpenGameType(Game g, Settings settings, Player opponent1, Player opponent2)
  {
    Log.debug(Log.TAG, "laying down");

    table.showPlayerCards(g, settings, opponent1);
    table.showPlayerCards(g, settings, opponent2);
  }

  public void onMovingRoundStart()
  {
  }

  @Override
  public void onPlayCard(Game g, Player player, Card card)
  {
    Profile profile = getCurrentProfile();

    table.hideClock();

    // start card animation, next process() will be called on animation end
    table.moveCardToTrick(player.id(), card, true, profile.settings.animationTime);

    process.delay = Settings.Delays.AFTER_CARD_PLAYED;
  }

  @Override
  public void onOpenPlayerCards(Game g, Settings settings, int id)
  {
    table.showPlayerCards(g, settings, id);
  }

/*
  public void onCardPlayed(Game g, Player mover, Player current, Card card, boolean automatic)
  {
    table.hideClock();

    // put the card on the corresponding trick card; at this point it's already done for human mover
    // unless the move is obvious and the player has the corresponding setting on
    // otherwise we do it only for android
    if (automatic)
      table.moveCardToTrick(current.id(), card, true);

    // AI plays misere - mark played card
    if (g.bid != null && g.bid.isMisere() && g.declarer != g.human() && g.declarer == current)
      table.markMisereDesk(card);

    process.delay = Process.Delay.AFTER_CARD_PLAYED;
  }
*/

  public void onBeforeMovingRoundEnd(Game g, Player mover)
  {
  }

  public void onAfterMovingRoundEnd(Game g, Settings settings, Player winner)
  {
    if (table != null)
    {
      table.updateTrickCount(g, winner);
      table.hideTrick(g, settings);
    }

    process.delay = Settings.Delays.AFTER_ROUND_END;
  }

  public void onBeforeScore(Game g)
  {
    table.hideTarget();
  }

  public void onTalonCardPlayed(Settings settings, int round)
  {
    table.openTalon(round);
  }

  public void onScore(Game g)
  {
    // wait for pool activity to close, then the processing should be resumed (see onActivityResult())
    g.options.set(Input.Options.WAIT_FOR_POOL);

    // show pool dialog
    showPoolDialog(false);
  }

  public void onShowInitialPosition(Game g, Settings settings)
  {
    g.options.set(Input.Options.WAIT_FOR_TOUCH);

    table.showInitialPosition(g, settings);

    GUI.toast(this, R.string.initial_position);
  }

  public void onSetOver(Game g)
  {
    saveOnCloud(g, CloudSave.EVERY_SET);
  }

  public void onGameOver(Game g)
  {
    saveOnCloud(g, CloudSave.EVERY_GAME);

    try
    {
      // send results to ferp center
      if (service != null)
        TaskGameAdd.execute(this, getCurrentProfile(), ServiceProxy.getSpeed(service));

      // show results
      Results.show(this);
      finish();
    }
    catch (ServiceException e)
    {
      exit(e);
    }
  }

  @Override
  public void onPoolDialogDismiss(Input.PoolUserChoice puc)
  {
    Game g = getCurrentProfile().game();

    // set input
    g.input.puc = puc;

    // remove the pool dialog to force its creation next time it opened - it must be recreated because of animation
    removeDialog(Dialogs.POOL);

    // resume processing in case we are in wait for pool mode, otherwise end the game
    if (g.options.isSet(Input.Options.WAIT_FOR_POOL))
      process.now();
  }

  @Override
  public void onSendReportRequested()
  {
    showDialog(Dialogs.REPORT_GAME);
  }

  @Override
  public void onShareClick()
  {
    showDialog(Dialogs.SHARE);
  }

  public void onCloseClick()
  {
    entrance();
  }

  // we get here when an offer or replay exit option is selected
  public void onItemSelected(int requestCode, int selected)
  {
    Profile profile = getCurrentProfile();

    switch (requestCode)
    {
      case Dialogs.RATE_APPLICATION:
        ApplicationRater.onOptionSelected(this, selected);

        break;

      case Dialogs.POLL:
        // !!! submit is performed automatically
        //PollManager.submit(this, selected);

        break;

      case Dialogs.EXIT_CONFIRMATION_REPLAY:
        switch (selected)
        {
          case ExitReplayOption.REPLAY_CANCEL:
            // in case resolver task is in progress - cancel it
            TaskResolver.cancel();

            profile.stopReplay();
            process.now();

            break;

          case ExitReplayOption.REPLAY_AGAIN:
            // in case resolver task is in progress - cancel it
            TaskResolver.cancel();

            profile.startReplay();
            process.now();

            break;

          case ExitReplayOption.EXIT_GAME:
            entrance();

            break;
        }

        break;
    }
  }

  @Override
  public void onItemSelected(Input.Offer offer)
  {
    Profile profile = getCurrentProfile();
    Game g = profile.game();

    g.input.score = profile.game().options.score;
    g.input.offer = offer;

    // continue processing
    process.now();
  }

  @Override
  public void onYesClick(int requestCode)
  {
    switch (requestCode)
    {
      case Dialogs.EXIT_CONFIRMATION_REGULAR:
        // finish the activity and go back to entrance screen
        entrance();

        break;

      case Dialogs.CONFIRM_BID_PLAY:
      case Dialogs.CONFIRM_BID_MISERE:
      case Dialogs.CONFIRM_BID_PASS:
      case Dialogs.CONFIRM_CONTRACT_WITHOUT3:
      case Dialogs.CONFIRM_CONTRACT_PLAY:
        Game g = getCurrentProfile().game();

        Game.log(g, "confirmed code " + requestCode + ", input=" + g.bid);
        process.now();

        break;
    }
  }

  @Override
  public void onNoClick(int requestCode)
  {
    switch (requestCode)
    {
      case Dialogs.CONFIRM_BID_PLAY:
      case Dialogs.CONFIRM_BID_MISERE:
      case Dialogs.CONFIRM_BID_PASS:
      case Dialogs.CONFIRM_CONTRACT_WITHOUT3:
      case Dialogs.CONFIRM_CONTRACT_PLAY:
        Game g = getCurrentProfile().game();

        Game.log(g, "not confirmed code " + requestCode);
        g.input.reset();

        break;
    }
  }

  @Override
  public void onSubmitPollResultsStarted()
  {
    showDialog(Dialogs.POLL_SUBMIT);
  }

  @Override
  public void onSubmitPollResultsEnded(boolean success, int bonus)
  {
    GUI.tryToDismissDialog(this, Dialogs.POLL_SUBMIT);
    GUI.toast(this, success ? R.string.poll_submit_success : R.string.poll_submit_failure);

    if (bonus > 0)
      BonusManager.instance(this).activateForPoll(bonus);
  }

  @Override
  protected Dialog onCreateDialog(int id)
  {
    switch (id)
    {
      case Dialogs.ALERT_ERROR:
        return new ErrorDialog(this, this);

      case Dialogs.NOT_ENOUGH_RESOURCES:
        return new ErrorDialog(this, this, R.string.dialog_not_enough_resources_message);

      case Dialogs.OFFER:
        return OfferDialog.create(this, this);

      case Dialogs.LAST_TRICK:
        return LastTrickDialog.create(this);

      case Dialogs.POOL:
        return PoolDialog.create(this, this);

      case Dialogs.REPORT_SUBMIT:
        ProgressDialog progress = new ProgressDialog(this);
        // allow dialog cancellation (the task will continue to execute)
        progress.setCancelable(true);
        progress.set(R.string.please_wait, R.string.report_game_progress);

        return progress;

      case Dialogs.REPORT_GAME:
        return new ReportGameDialog(this, this);

      case Dialogs.RATE_APPLICATION:
        return new RadioListViewDialog(this, this, id);

      case Dialogs.EXIT_CONFIRMATION_REGULAR:
        return new MessageDialog(this, this, id);

      case Dialogs.EXIT_CONFIRMATION_REPLAY:
        return new RadioListViewDialog(this, this, id);

      case Dialogs.FAST_MOVING:
        return new ProgressDialog(this).set(R.string.please_wait, R.string.fast_moving);

      case Dialogs.CONFIRM_BID_PLAY:
      case Dialogs.CONFIRM_BID_MISERE:
      case Dialogs.CONFIRM_BID_PASS:
      case Dialogs.CONFIRM_CONTRACT_WITHOUT3:
      case Dialogs.CONFIRM_CONTRACT_PLAY:
        return ConfirmationDialog.create(this, this, id);

      case Dialogs.SHARE:
        return ShareDialog.create(this);

      case Dialogs.TRICKING_10_CHECK:
        return new ProgressDialog(this).set(R.string.please_wait, R.string.dialog_tricking10_check_message);

      case Dialogs.POLL:
        return PollDialog.create(this, this);

      case Dialogs.POLL_SUBMIT:
        return new ProgressDialog(this).set(R.string.please_wait, R.string.poll_submit_progress);
    }

    return super.onCreateDialog(id);
  }

  @Override
  protected void onPrepareDialog(int id, Dialog dialog)
  {
    Profile profile = getCurrentProfile();

    Preferences.setLocale(this, profile);

    super.onPrepareDialog(id, dialog);

    switch (id)
    {
      case Dialogs.OFFER:
        ((OfferDialog)dialog).prepare();
        break;

      case Dialogs.LAST_TRICK:
        ((LastTrickDialog)dialog).prepare();
        break;

      case Dialogs.REPORT_GAME:
        ((ReportGameDialog)dialog).reset();
        break;

      case Dialogs.RATE_APPLICATION:
        ((RadioListViewDialog)dialog).set(getString(R.string.rate_title), R.string.rate_text, Arrays.asList(getResources().getStringArray(R.array.rate_types)));
        break;

      case Dialogs.EXIT_CONFIRMATION_REGULAR:
        ((MessageDialog)dialog).set(R.string.warning, getResources().getString(R.string.warning_exit_game));
        break;

      case Dialogs.EXIT_CONFIRMATION_REPLAY:
        ((RadioListViewDialog)dialog).set(getResources().getString(R.string.dialog_exit_replay_title), Arrays.asList(getResources().getStringArray(R.array.dialog_exit_replay_options)));
        break;

      case Dialogs.CONFIRM_BID_PLAY:
        ((ConfirmationDialog)dialog).set(R.string.dialog_confirmation_title_bid_play,
                                         getResources().getString(R.string.dialog_confirmation_text_bid_play1),
                                         profile.game().input.bid,
                                         getResources().getString(R.string.dialog_confirmation_text_bid_play2),
                                         profile.settings.confirmations, ConfirmationDialog.bcc, profile.settings.confirmations.bid);
        break;

      case Dialogs.CONFIRM_BID_MISERE:
        ((ConfirmationDialog)dialog).set(R.string.dialog_confirmation_title_bid_misere, getResources().getString(R.string.dialog_confirmation_text_bid_misere), profile.settings.confirmations, ConfirmationDialog.bcc, profile.settings.confirmations.bid);
        break;

      case Dialogs.CONFIRM_BID_PASS:
        ((ConfirmationDialog)dialog).set(R.string.dialog_confirmation_title_bid_pass, getResources().getString(R.string.dialog_confirmation_text_bid_pass), profile.settings.confirmations, ConfirmationDialog.bcc, profile.settings.confirmations.bid);
        break;

      case Dialogs.CONFIRM_CONTRACT_WITHOUT3:
        ((ConfirmationDialog)dialog).set(R.string.dialog_confirmation_title_contract_without3, getResources().getString(R.string.dialog_confirmation_text_contract_without3), profile.settings.confirmations, ConfirmationDialog.ccc, profile.settings.confirmations.contract);
        break;

      case Dialogs.CONFIRM_CONTRACT_PLAY:
        ((ConfirmationDialog)dialog).set(R.string.dialog_confirmation_title_contract_play,
            getResources().getString(R.string.dialog_confirmation_text_contract_play1),
            profile.game().input.bid,
            getResources().getString(R.string.dialog_confirmation_text_contract_play2),
            profile.settings.confirmations, ConfirmationDialog.ccc, profile.settings.confirmations.contract);
        break;

      case Dialogs.SHARE:
        ((ShareDialog)dialog).prepare();
        break;

      case Dialogs.POLL:
        ((PollDialog)dialog).prepare();
        break;
    }
  }

  @Override
  protected void onPrepareDialog(int id, Dialog dialog, Bundle args)
  {
    super.onPrepareDialog(id, dialog, args);

    switch (id)
    {
      case Dialogs.POOL:
        ((PoolDialog)dialog).set(args.getBoolean(MENU));
        break;
    }
  }

  public void onCardReleased(Card c)
  {
    Profile profile = getCurrentProfile();
    Game g = profile.game();

    // hide glove
    table.hideTurn();
    //table.showClock(g.current.player);

    table.enablePlayerCards(g.player.current, false);
    table.moveCardToTrick(g.player.current, c, false, profile.settings.animationTime);
  }

  @Override
  public void onCardCommit(Card card)
  {
    Game g = getCurrentProfile().game();

    //Log.debug(Log.TAG, "card animation ended");
    g.input.card = card.hash;

    // if still resolving do not process
    if (TaskResolver.isResolving())
    {
      Log.debug(Log.TAG, "resolving is still in process!");

      table.showClock(g.player.current);
    }
    else
      process.now();
  }

  public void onTouch()
  {
    Input.Options options = getCurrentProfile().game().options;
    // continue processing
    if (options.isSet(Input.Options.WAIT_FOR_TOUCH))
    {
      // clear options
      options.clear();

      process.now();
    }
  }

  public void onOfferButtonPressed()
  {
    showDialog(Dialogs.OFFER);
  }

  public void onOfferDeclined()
  {
    GUI.toast(this, R.string.offer_denied);
  }

  @Override
  public void onTrickingTenCheckRequested(Game g)
  {
    Profile p = getCurrentProfile();

    if (service != null)
      new TaskTrickingTenCheck(this, g, p.settings, service).execute();
  }

  @Override
  public void onTrickingTenCheckStarted(Game g)
  {
    showDialog(Dialogs.TRICKING_10_CHECK);
  }

  @Override
  public void onTrickingTenCheckFinished(Game g)
  {
    GUI.tryToDismissDialog(this, Dialogs.TRICKING_10_CHECK);

    if (!g.options.isHumanInputRequired())
      process.now();
  }

  public void onInputButtonClicked(View view, int id)
  {
    if (view.isShown())
    {
      Profile profile = getCurrentProfile();
      Game g = profile.game();

      switch (id)
      {
        case R.id.pass:
          g.input.bid.type = Bid.Type.PASS;
          if (confirm(Dialogs.CONFIRM_BID_PASS, profile.settings.confirmations.bid, Confirmation.pass))
            return;
          break;

        case R.id.whist_pass:
          g.input.bid.type = Bid.Type.PASS;
          break;

        case R.id.whist_half:
          g.input.bid.type = Bid.Type.HALF_WHIST;
          break;

        case R.id.whist_full:
          g.input.bid.type = Bid.Type.WHIST;
          break;

        case R.id.misere:
          g.input.bid.type = Bid.Type.MISERE;
          if (confirm(Dialogs.CONFIRM_BID_MISERE, profile.settings.confirmations.bid, Confirmation.misere))
            return;
          break;

        case R.id.bid:
          g.input.bid.assign(profile.game().options.bid);
          if (confirm(Dialogs.CONFIRM_BID_PLAY, profile.settings.confirmations.bid, Confirmation.play))
            return;
          break;

        case R.id.drop:
          Card[] drop = table.getDrop();

          // assign dropped cards
          for (int i = 0; i < drop.length; i++)
            g.input.drop[i] = drop[i].hash;

          Game.log(profile.game(), "drop button");
          break;

        case R.id.declare:
          g.input.bid.assign(profile.game().options.bid);
          if (confirm(Dialogs.CONFIRM_CONTRACT_PLAY, profile.settings.confirmations.contract, Confirmation.contract))
            return;
          Game.log(profile.game(), "declare button, declared " + g.input.bid);
          break;

        case R.id.stand:
        case R.id.stand2:
          g.input.stand = true;
          break;

        case R.id.lay_down:
        case R.id.lay_down2:
          g.input.stand = false;
          break;

        case R.id.without3:
          g.input.bid.type = Bid.Type.WITHOUT_3;
          if (confirm(Dialogs.CONFIRM_CONTRACT_WITHOUT3, profile.settings.confirmations.contract, Confirmation.without3))
            return;
          break;

        case R.id.tricking10_agree:
          g.input.check10 = false;
          break;

        case R.id.tricking10_play:
          g.input.check10 = true;
          break;

        case R.id.misere_undo:
          g.input.misere = false;
          break;

        case R.id.misere_play:
          g.input.misere = true;
          break;
      }

      // process the current state if confirmation not required
      process.now();
    }
  }

  public void onUndoDrop(Game g)
  {
    Dropping.Undo.execute(g);

    process.now();
  }

  public void onBestScoreRequested(Game g) throws Game.Error
  {
    if (service != null)
      try
      {
        int score = ServiceProxy.getBestScore(service);

        g.options.score = (short)score;
      }
      catch (ServiceException e)
      {
        throw new Game.Error(e);
      }
  }

  @SuppressWarnings("unchecked")
  public void onSolveRequested(Game g, Player mover) throws Game.Error
  {
    // show glove when human is mover and sand clock when its AI
    if (mover == g.human())
      table.showTurn(g.player.current);
    else
      if (service != null)
        try
        {
          int score = ServiceProxy.getBestScore(service);

          // show clock only when resolver still really needs time to complete the task
          if (!Score.isComplete((short)score))
            table.showClock(mover.id());

          // start resolver task
          TaskResolver.start(this, service, mover);
        }
        catch (ServiceException e)
        {
          throw new Game.Error(e);
        }
  }

  public void onTreeReady(Player mover) throws Game.Error
  {
    Profile profile = getCurrentProfile();
    Game g = profile.game();
    int depth = Levels.getBestMoveDepth(profile.game(), profile.settings, mover);

    if (table != null && service != null && !stopped)
    {
      table.hideClock();

      try
      {
        byte move = ServiceProxy.getBestMove(service, depth, g.hand, g.trick.current, g.getCurrentPlayerHand().current);
        Card card = Card.card(move);

        if (mover == g.human())
        {
          Log.debug(Log.TAG, "suggestion for " + g.player(g.player.current) + " [" + Hand.dump(g.getCurrentPlayerHand().current) + "]: " + card + ", mover: " + mover.id());

          if (g.input.card == 0)
            Log.debug(Log.TAG, "waiting for the human...");
          else
            process.now();
        }
        else
          onPlayCard(g, g.current(), card);
      }
      catch (ServiceException e)
      {
        throw new Game.Error(e);
      }
    }
  }

  public void onResolverInitialize(Game g, int evaluator, int declarer, int starter, int trump, int hand0, int hand1, int hand2, byte talon0, byte talon1, int dropReal, int dropGuessed, boolean open) throws Game.Error
  {
    if (service != null)
    {
      // hide first hand only if game type determined
      if (g.type != Game.Type.UNKNOWN)
        table.hideFirstHand();

      try
      {
        ServiceProxy.initialize(service, evaluator, declarer, starter, trump, hand0, hand1, hand2, talon0, talon1, dropReal, dropGuessed, 0, open, getCurrentProfile().settings.isRostov(), g.service);
      }
      catch (ServiceException e)
      {
        throw new Game.Error(e);
      }
    }
  }

  @Override
  public void onStandingGameType(Game g, int hand0, int hand1, int hand2, int trick) throws Game.Error
  {
    // show toast only if AI whists
    if (g.whisters == 1 && g.passers == 1 && g.opponent() != g.human())
      GUI.toast(this, R.string.playing_blindly, Toast.LENGTH_LONG);

    if (service != null)
      try
      {
        // recalculate the tree
        ServiceProxy.recalculateOnNextSolve(service, hand0, hand1, hand2, trick);
      }
      catch (ServiceException e)
      {
        throw new Game.Error(e);
      }
  }

  public void onResolverAdvance(Game g, Settings settings, int move) throws Game.Error
  {
    if (service != null)
      try
      {
        // AI plays misere - mark played card
        if (g.bid != null && g.bid.isMisere() && g.declarer() != g.human() && g.player.declarer == g.player.current)
          table.markMisereDesk(Card.card(move));

        // redraw player's hand to maintain suit order
        table.showPlayerCards(g, settings, g.player.current);

        // we get there after card played, schedule next process with a delay
        process.delay = Settings.Delays.AFTER_CARD_PLAYED;

        ServiceProxy.advance(service, g.hand, g.trick.current);
      }
      catch (ServiceException e)
      {
        throw new Game.Error(e);
      }
  }

  @Override
  public void onReplayStart()
  {
    GUI.toast(this, R.string.toast_replay_start);
  }

  @Override
  public void onReplayEnd()
  {
    GUI.toast(this, R.string.toast_replay_end);
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item)
  {
    switch (item.getItemId())
    {
      case R.id.main_pool:
        showPoolDialog(true);
        return true;

      case R.id.main_last_trick:
        showDialog(Dialogs.LAST_TRICK);
        return true;

      case R.id.main_score:
        startActivity(new Intent(this, Results.class));
        return true;

      case R.id.main_settings:
        // create intent
        Intent intent = new Intent(this, Preferences.class);

        // create bundle (settings are always disable when opening from game)
        Bundle b = new Bundle();
        b.putBoolean(Preferences.Intent.NEW_GAME, false);
        b.putBoolean(Preferences.Intent.ENTRANCE, false);

        // bind bundle to an intent
        intent.putExtras(b);

        // start Preference activity
        startActivityForResult(intent, PREFERENCES);

        return true;

      case R.id.main_report_game:
        showDialog(Dialogs.REPORT_GAME);
        return true;

      case R.id.main_help:
        startActivity(new Intent(this, Help.class));
        return true;

      default:
        return super.onOptionsItemSelected(item);
    }
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu)
  {
    getMenuInflater().inflate(R.menu.main, menu);

    menu.findItem(R.id.main_pool).setIcon(android.R.drawable.ic_menu_edit);
    menu.findItem(R.id.main_score).setIcon(R.drawable.ic_menu_score);
    menu.findItem(R.id.main_settings).setIcon(android.R.drawable.ic_menu_preferences);
    menu.findItem(R.id.main_help).setIcon(android.R.drawable.ic_dialog_info);

    menuPreviousTrick = menu.findItem(R.id.main_last_trick);
    menuPreviousTrick.setIcon(android.R.drawable.ic_menu_view);
    menu.findItem(R.id.main_report_game).setIcon(android.R.drawable.ic_menu_send);

    return true;
  }

  @Override
  public boolean onPrepareOptionsMenu(Menu menu)
  {
    // set correct enable state to 'Previous trick' option each time the context menu is opened
    menuPreviousTrick.setEnabled(getCurrentProfile().game().trick.last != 0);

    return true;
  }

  @Override
  public void onBackPressed()
  {
    showDialog(getCurrentProfile().game().replay ? Dialogs.EXIT_CONFIRMATION_REPLAY : Dialogs.EXIT_CONFIRMATION_REGULAR);
  }

  @Override
  public void onConfigurationChanged(Configuration newConfig)
  {
    Preferences.setLocale(this, getCurrentProfile());

    super.onConfigurationChanged(newConfig);
  }

  public void onSendingGameReportStart()
  {
    showDialog(Dialogs.REPORT_SUBMIT);
  }

  public void onSendingGameReportEnd(boolean success)
  {
    GUI.tryToDismissDialog(this, Dialogs.REPORT_SUBMIT);
    GUI.toast(this, success ? R.string.report_game_success : R.string.report_game_failure);
  }

  public void onSendClick(String email, String description)
  {
    new TaskGameReportSend(getApplication(), getCurrentProfile(), email, description, null, this).execute();
  }

  @Override
  public void finish()
  {
    destroyTable();

    super.finish();
  }

  @Override
  public void onDeckLoadStart(String deck)
  {
    LoadingDialog.showDeck(this, deck);
  }
  @Override
  public void onDeckLoadFinished(DeckManager dm, String deck) {tryToResumeGame();}

  @Override
  protected void onCreate(Bundle savedInstanceState)
  {
    Profile profile = getCurrentProfile();

    // set correct orientation depends on if running on tablet or not
    GUI.setOrientation(this, profile);

    super.onCreate(savedInstanceState);

    // this is to prevent incorrect activity restoring, when profile or game objects might be null
    if (profile == null || profile.game() == null)
    {
      Log.debug(Log.TAG, "Ferp activity finished. Profile = " + profile + ", game = " + (profile == null ? "n/a" : profile.game()));
      entrance();

      return;
    }

    // restore state
    String message = "ferp activity " + (savedInstanceState == null ? "created" : "restored") + ", pid=" + android.os.Process.myPid() + ", tid=" + Thread.currentThread().getId();

    Log.debug(Log.TAG, message);
    Game.log(profile.game(), message);

    // show the inflated main layout
    Preferences.setLocale(this, profile);

    setContentView(R.layout.main);

    // no ads in ebook
    //MediationAdManager.initialize(this);
    InterstitialAdManager.initialize(this);

    // find table view
    table = (TableView)findViewById(R.id.table);
    table.initialize(this);

    // start the resolver
    startService(new Intent(this, ResolverService2.class));

    // report launch
    ApplicationRater.onAppLaunch(this, Dialogs.RATE_APPLICATION);

/*
    try
    {
      PackageInfo info = getPackageManager().getPackageInfo("ferp.android", PackageManager.GET_SIGNATURES);

      for (Signature signature : info.signatures)
      {
        MessageDigest md = MessageDigest.getInstance("SHA");
        md.update(signature.toByteArray());
        Log.debug(Log.TAG, "KeyHash: " + Base64.encodeToString(md.digest(), Base64.DEFAULT));
      }
    } catch (PackageManager.NameNotFoundException e) {

    } catch (NoSuchAlgorithmException e) {

    }
*/
  }

  @Override
  protected void onResume()
  {
    Log.debug(Log.TAG, "resuming activity");

    MediationAdManager.resume();

    super.onResume();
  }

  @Override
  protected void onPause()
  {
    Log.debug(Log.TAG, "pausing activity");

    MediationAdManager.pause();

    super.onPause();
  }

  @Override
  protected void onRestart()
  {
    restarting = true;

    super.onRestart();
  }

  @Override
  protected void onStart()
  {
    Profile profile = getCurrentProfile();
    String message = (restarting ? "re" : "") + "starting activity";

    Log.debug(Log.TAG, message);
    Game.log(profile.game(), message);

    // clear restarting flag
    restarting = false;
    // activity is not stopped anymore
    stopped = false;
    // game is not resumed yet
    resumed = false;

    // clear messages and animations from previous cycle
    removeEvents();

    // replace the processing object
    process = new Process();

    // load card decks asynchronously
    TaskDeckLoader.execute(this, this, profile.settings.deck);
    // bind to service
    bindService(new Intent(this, ResolverService2.class), connection, Context.BIND_AUTO_CREATE);

    super.onStart();
  }

  @Override
  protected void onStop()
  {
    String message = "stopping activity";

    Log.debug(Log.TAG, message);
    Game.log(getCurrentProfile().game(), message);

    // remove events to prevent processing
    removeEvents();
    // unbind the resolver service
    tryToUnbindService();

    stopped = true;

    super.onStop();
  }

  @Override
  protected void onDestroy()
  {
    // destroy the ADs
    MediationAdManager.destroy();

    destroyTable();
    // cancel resolver task if running
    TaskResolver.cancel();
    // stop the resolver service
    stopService(new Intent(this, ResolverService2.class));

    Log.debug(Log.TAG, "ferp activity destroyed");

    super.onDestroy();
  }

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data)
  {
    super.onActivityResult(requestCode, resultCode, data);

    switch (requestCode)
    {
      case PREFERENCES:
        applySettings();
        break;

      case Connector.RequestCode.VK:
        Connector.vk.onActivityResult(this, resultCode, data);
        break;

      case Connector.RequestCode.TW:
        Connector.tw.onActivityResult(this, resultCode, data);
        break;

      case Connector.RequestCode.FB:
        Connector.fb.onActivityResult(this, resultCode, data);
        break;

    }
  }

  private void applySettings()
  {
    Profile profile = getCurrentProfile();
    Game g = profile.game();

    // reorder card views as suit order or rank order might have changed
    table.reorderCards(g, profile.settings);

    // switch rank order on misere desk as it might have changed
    if (g.bid != null && g.bid.isMisere() && g.declarer() != g.human())
      table.reorderMisereDesk();
  }

  private void entrance()
  {
    // close the activity
    finish();
    // start entrance
    startActivity(new Intent(getApplicationContext(), Entrance.class));
  }

  private void destroyTable()
  {
    // remove postDelayed runnable, otherwise the scheduled event might arrive
    // when reopen the activity too fast and raise condition could happen
    removeEvents();

    DeckManager.instance(this).clear();

    // trigger memory release
    table = null;

    Log.debug(Log.TAG, "Table destroyed");
  }

  private void exit(Exception e, String where, int dialog, boolean report)
  {
    if (!exiting)
    {
      // perform exit logic only once
      exiting = true;
      // set paused flag to false to prevent processing
      stopped = true;

      // remove all callbacks and animations
      removeEvents();

      // remove all dialogs
      for (int id : Dialogs.set)
        removeDialog(id);

      TaskResolver.cancel();

      showDialog(dialog);

      if (report)
        new TaskGameReportSend(getApplication(), getCurrentProfile(), ReportGameDialog.getEmail(getApplication(), "runtime@exception.com"), where, e, null).execute();

      if (e != null)
        Log.error(Log.TAG + "/Exit", e);
    }
  }

  private void showPoolDialog(boolean menu)
  {
    Bundle b = new Bundle();

    b.putBoolean(MENU, menu);

    showDialog(Dialogs.POOL, b);
  }

  private boolean confirm(int dialogID, Settings.Confirmation confirmation, Confirmation c)
  {
    Profile profile = getCurrentProfile();
    Game g = profile.game();

    if (c.isRequired(confirmation, g, profile.settings, g.input.bid))
    {
      showDialog(dialogID);
      return true;
    }

    return false;
  }

  private Profile getCurrentProfile() {return ProfileManager.instance(this).getCurrentProfile();}

  private void tryToResumeGame()
  {
    Log.debug(Log.TAG, "trying to resume game: p/" + stopped + ", r/" + resumed + ", s/" + service + ", dm/" + DeckManager.instance(this).isReady());

    if (!stopped && !resumed && service != null && DeckManager.instance(this).isReady())
    {
      Profile profile = getCurrentProfile();

      // start processing
      if (profile != null && profile.game() != null)
      {
        // start preallocate task
        new TaskPreallocator(Ferp.this, service).execute();

        // resume game
        profile.resumeGame();
        process.schedule(0);
      }
      else
        entrance();

      resumed = true;
    }
  }

  private void tryToUnbindService()
  {
    if (service != null)
    {
      unbindService(connection);

      service = null;
    }
  }

  private void removeEvents()
  {
    if (table != null)
    {
      table.removeCallbacks(process);
      table.removeAnimations();
    }
  }

  private void saveOnCloud(Game g, Settings.CloudSave[] cloudSaves)
  {
    Profile profile = getCurrentProfile();

    if (g.replay || profile.hacked)
      return;

    for (Settings.CloudSave cs : cloudSaves)
      if (profile.settings.cloudSave == cs)
      {
        CloudManager.instance(this).save(profile);

        return;
      }
  }

  // perform delayed processing in the main UI thread
  private class Process implements Runnable
  {
    public long delay;

    // process now
    public void now()
    {
      // clear options
      //getCurrentProfile().game().options.clear();

      // hide options first and process almost immediately
      if (table != null)
      {
        table.hideOptions();
        table.postDelayed(this, 1);
      }
    }

    // schedule delayed processing
    public void schedule(long delay)
    {
      Log.debug(Log.TAG, "scheduling next process in " + delay  + " ms");

      // schedule the processing event
      if (table != null)
        table.postDelayed(this, delay);
    }

    public void run()
    {
      // set delay to the default
      delay = Settings.Delays.DEFAULT;

      Profile profile = getCurrentProfile();
      Game g = profile.game();

      if (g != null && !stopped && this == process)
      {
        Log.debug(Log.TAG, "process run");

        Settings settings = profile.settings;
        State state = g.state();

        try
        {
          String m = g.mode() == null ? null : g.mode().getClass().getSimpleName();
          String s = g.state() == null ? null : g.state().getClass().getSimpleName();
          String pid = g.current() == null ? null : String.valueOf(g.current().id());

          Game.log(g, "processing: mode=" + m + ", state=" + s + ", player=" + pid + ", " +
                             "options=[f/" + g.options.flag() + ",h/" + g.options.hand + ",b/" + g.options.bid + ",s/" + g.options.score + "], " +
                             "input=[b/" + g.input.bid + ",p/" + g.input.puc + ",o/" + g.input.offer + ",d/" + g.input.drop[0] + "&" + g.input.drop[1] + ",c/" + g.input.card + "]");

          int r = state.process(Ferp.this, profile, g, settings, g.input);

          switch (r)
          {
            case State.Result.SHOW_OPTIONS:
              Log.debug(Log.TAG, "showing input options");

              if (table != null)
                table.showOptions(g, settings);

              break;

            // should be 'default' to cover SCHEDULE and different delays
            default:
              schedule(Math.max(r, delay));

              break;
          }

          TaskResolver.setGUIReady();
        }
        catch (Game.Error error)
        {
          if (error.getCause() instanceof ServiceException)
            exit((ServiceException)error.getCause());
          else
            exit(error, "processing stopped because of an error!", Dialogs.ALERT_ERROR, true);
        }
        catch (RuntimeException e)
        {
          exit(e, "processing stopped because of a runtime exception!", Dialogs.ALERT_ERROR, true);
        }
      }
      else
      {
        String message = "process run skipped: p/" + stopped + ", g/" + g + ", same process/" + (this == process);

        Log.debug(Log.TAG, message);
        Game.log(getCurrentProfile().game(), message);
      }
    }
  }

  private static final class Dialogs
  {
    // error dialog id
    private static final int ALERT_ERROR = 1;
    // not enough memory dialog id
    private static final int NOT_ENOUGH_RESOURCES = 2;

    // offer dialog id
    private static final int OFFER = 3;
    // previous trick dialog id
    private static final int LAST_TRICK = 4;
    // pool dialog id
    private static final int POOL = 5;
    // progress dialog
    private static final int REPORT_SUBMIT = 6;
    // report game
    private static final int REPORT_GAME = 7;
    // rate application
    private static final int RATE_APPLICATION = 8;
    // exit confirmation dialogs
    private static final int EXIT_CONFIRMATION_REGULAR = 9;
    private static final int EXIT_CONFIRMATION_REPLAY = 10;
    // fast moving
    private static final int FAST_MOVING = 12;

    // confirmation dialogs
    private static final int CONFIRM_BID_PLAY = 13;
    private static final int CONFIRM_BID_MISERE = 14;
    private static final int CONFIRM_BID_PASS = 15;
    private static final int CONFIRM_CONTRACT_WITHOUT3 = 16;
    private static final int CONFIRM_CONTRACT_PLAY = 17;

    // share
    public static final int SHARE = 18;

    // tricking '10' check dialog
    private static final int TRICKING_10_CHECK = 19;

    // poll dialog
    private static final int POLL = 20;
    // poll submit dialog
    private static final int POLL_SUBMIT = 21;

    // all dialogs id's set
    private static final int set[] = new int[] {
        ALERT_ERROR, NOT_ENOUGH_RESOURCES, OFFER, LAST_TRICK, POOL, REPORT_SUBMIT, REPORT_GAME, RATE_APPLICATION,
        EXIT_CONFIRMATION_REGULAR, EXIT_CONFIRMATION_REPLAY, FAST_MOVING, CONFIRM_BID_PLAY, CONFIRM_BID_MISERE,
        CONFIRM_BID_PASS, CONFIRM_CONTRACT_WITHOUT3, CONFIRM_CONTRACT_PLAY, SHARE, TRICKING_10_CHECK, POLL, POLL_SUBMIT
    };
  }

  private static final class ExitReplayOption
  {
    private static final int REPLAY_CANCEL = 0;
    private static final int REPLAY_AGAIN  = 1;
    private static final int EXIT_GAME     = 2;
  }

  // predefined cloud save settings options arrays
  private static final class CloudSave
  {
    private static final Settings.CloudSave[] EVERY_SET  = {Settings.CloudSave.EVERY_SET};
    private static final Settings.CloudSave[] EVERY_GAME = {Settings.CloudSave.EVERY_SET, Settings.CloudSave.EVERY_GAME};
  }

  // preferences request code
  private static final int PREFERENCES = 1;
  // menu pressed bundle key
  public static final String MENU = "menu";

  private TableView table;
  private Process process = new Process();
  private boolean stopped;
  private boolean resumed;
  private boolean restarting;
  private boolean exiting;

  private MenuItem menuPreviousTrick;

  // resolver service interface
  private IResolverService2 service;
  private ServiceConnection connection = new ServiceConnection()
  {
    // called when the connection with the service is established
    public void onServiceConnected(ComponentName className, IBinder binder)
    {
      if (!exiting)
      {
        service = IResolverService2.Stub.asInterface(binder);

        Log.debug(Log.TAG, "service connected");

        // resume game
        if (service == null)
          exit(null, "onServiceConnected: Stub.asInterface returned null!", Dialogs.ALERT_ERROR, true);
        else
          tryToResumeGame();
      }
    }

    // called when the connection with the service disconnects unexpectedly
    public void onServiceDisconnected(ComponentName className)
    {
      Log.debug(Log.TAG, "service has unexpectedly disconnected, thread " + Thread.currentThread().getId());

      service = null;

      exit(null, "onServiceDisconnected", Dialogs.NOT_ENOUGH_RESOURCES, false);
    }
  };
}
